package cyce.peer;

import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.rmi.ConnectException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.server.Unreferenced;
import java.util.LinkedList;
import java.util.List;

import cyce.file.Directory;
import cyce.file.File;
import cyce.file.FileHasher;
import cyce.file.FileFactory;
import cyce.file.FilePacket;
import cyce.peer.filechooser.FileModel.FileTreeModel;
import cyce.server.IPeerServer;
import cyce.server.NotPrimaryServerException;

/**
 * This class implements the Peer functionality in the distributed file system
 * @author CYCE
 *
 */
public class Peer implements IServerPeer, Unreferenced
{
	private String fileStore;
	
	private IPeerServer server = null;
	
	private int primaryServerPort = -1;
	private String primaryServerName = null; 
	int secondaryServerPort = -1;
	private String secondaryServerName = null;
	private IServerPeer thisPeerRemote;
	
	private List<File> localFiles = new LinkedList<File>();
	
	FileTreeModel treeModel;
	
	Directory root;
	
	String xmlRootDumpFile;
	
	public Peer(String primaryServerName, 
			int primaryServerPort, 
			String secondaryServerName, 
			int secondaryServerPort,
			String localFileStore,
			String rootDumpFile,
			Directory root) throws RemoteException
	{
		this.xmlRootDumpFile = rootDumpFile;
		
		this.fileStore = localFileStore;
		this.root = root;
		this.primaryServerName = primaryServerName;
		this.primaryServerPort = primaryServerPort;
		this.secondaryServerName = secondaryServerName;
		this.secondaryServerPort = secondaryServerPort;
		
		thisPeerRemote = (IServerPeer) UnicastRemoteObject.exportObject(this, 0);
		
		buildLocalFileList();
		connectToServer();
	}
	
	private void buildLocalFileList(){
		localFiles.clear();
		
		//the directory from the file store
		java.io.File localDir = new java.io.File(fileStore);
		
		if(!localDir.exists()){
			try {
				localDir.mkdirs();
			} catch (SecurityException e) {
				e.printStackTrace();
			}
			return;
		}
		
		//add to local files
		LocalContentFilter filter = new LocalContentFilter(localDir);
		String[] contents = localDir.list(filter);
		
		for (String fileName : contents){
			File file = root.getFileByHash(fileName);
			if (file!=null){
				localFiles.add(file);
			}
		}
	}
	
	/**
	 * If there is a connection to the server, request content from the server.
	 * Otherwise, attempt to connect to the primary server. If the primary
	 * server is down, try to connect to the secondary server.
	 * @return Returns true if we are connected.
	 */
	private synchronized boolean connectToServer(){
		
		//test if connected
		if (server!=null){
			try {
				server.requestContent(null);
				return true;
			} catch (ConnectException e){
				server = null;
				// we're not connected
			} catch (NotPrimaryServerException e){
				// we're connected, but to the wrong server
				server = null;
			} catch (RemoteException e) {
				//an error occured, but we're still connected
				return true;
			}
		}
		
		//if not connected
			//attempt connection to primary
			try {
				Registry registry = LocateRegistry.getRegistry(this.primaryServerName,this.primaryServerPort);
				server = (IPeerServer) registry.lookup("server");
				this.root = server.register(thisPeerRemote, localFiles);
				System.err.println("Connected to Primary Server");
				return true;
			} catch (Exception e) {
				//could not connect to primary server
				//fall through, attempt connection to secondary
			}
			
			//attempt connection to secondary
			try {
				Registry registry = LocateRegistry.getRegistry(this.secondaryServerName,this.secondaryServerPort);
				server = (IPeerServer) registry.lookup("server");
				this.root = server.register(thisPeerRemote, localFiles);
				System.err.println("Connected to Secondary Server");
				return true;
			} catch (Exception e) {
				//could not connect to secondary server
			}
			return false;
	}
	
	/**
	 * Download a file from peers. If the file is already saved locally, it
	 * will retrieve the file locally and not connect to the server. 
	 * @param file The file you wish to download
	 * @param localPath The localpath you wish to download the file
	 * @throws RemoteException
	 * @throws IOException
	 */
	public void getFileFromPeers(File file, String localPath) throws RemoteException, IOException
	{	
		//determine if file is stored locally
		if (localFiles.contains(file)){
			FilePacket localFile = this.getFile(file.getHash());
			
			if (localFile!=null){
				// save the file
				saveFile(localFile, localPath);
				return;
			}

		}
		
		if (!connectToServer()) {
			throw new RemoteException("Could not connect to network. Try again later.");
		}
		
		// Get list of peers with this file
		List<IPeer> peers;
		try {
			peers = server.requestContent(file);
		} catch (NotPrimaryServerException e1) {
			// connected to the wrong server, attempt connection to primary
			if (!connectToServer()){
				throw new RemoteException("Could not connect to network. Try again later.");
			}
			try{
				peers = server.requestContent(file);
			} catch (NotPrimaryServerException e2) {
				//fail if it doesn't work the second time
				throw new RemoteException("Could not connect to network. Try again later.");
			}
		}		
		
		// Get the file
		FilePacket receivedFile = null;
		for (int i = 0; i < peers.size(); i++)
		{
			IPeer peer = peers.get(i);
			
			try
			{
				receivedFile = peer.getFile(file.getHash());
				break;
			}
			catch (RemoteException e)
			{
				// follow through, try next peer
			}			
		}		
		
		if (receivedFile == null){
			throw new RemoteException("Could not retrieve content from any peer.");
		}
		
		
		// Try saving file at localPath..throw exception if it failed
		saveFile(receivedFile, localPath);
		// Try saving in local content store
		saveFile(receivedFile, getFilename(file.getHash()));
		
		//	Tell server you now have the content
		try {
			server.advertiseFile(this, file);
			localFiles.add(file);
		} catch (NotPrimaryServerException e) {
			//connected to the wrong server, attempt connection to primary
			if (!connectToServer()){
				//do not fail here - the main operation actually succeeded
			}
			try{
				server.advertiseFile(this, file);
				localFiles.add(file);
			} catch (NotPrimaryServerException e2) {
			//do not fail here - the main operation actually succeeded
			}
		}
	}
	
	/**
	 * Save the file packet to the requested location
	 * @param filePacket File packet containing the data for the file
	 * @param localPath Local path of where the file should be saved 
	 * @throws IOException
	 */
	private void saveFile(FilePacket filePacket, String localPath) throws IOException
	{
		// Save to requested location	
		FileOutputStream fos = new FileOutputStream(localPath);
		filePacket.writeTo(fos);	
		fos.close();
	}

	/**
	 * Retrieve the file packet information for the file for uploading
	 * to other peers.
	 * @param fileHash The file hash name
	 * @return The file packet containing the data for the file
	 */
	public FilePacket getFile(String fileHash) throws RemoteException 
	{		
		FilePacket packet = new FilePacket(getFilename(fileHash));
		try {
			packet.readIn();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			throw new RemoteException("Could not retrieve file.");
		}
		return packet;
	}
	
	public String getFilename(String fileHash)
	{		
		java.io.File path = new java.io.File(fileStore);
		return path.getAbsolutePath() + java.io.File.separator + fileHash;
	}

	/**
	 * Refreshes the distributed file system GUI
	 * @param root Directory root 
	 */
	public void updateFileList(Directory root) {
		this.root = root;
		if (this.treeModel!=null){
			this.treeModel.updateTree();
		}
		//write out the current contents
		if (this.xmlRootDumpFile != null){
			String xml = FileFactory.serialize(root);
			try {
				BufferedWriter writer = new BufferedWriter(new FileWriter(this.xmlRootDumpFile));
				writer.write(xml);
				writer.close();
			}
			catch (FileNotFoundException fnfe) {
				fnfe.printStackTrace();
				return;
			}
			catch (IOException ioe) {
				ioe.printStackTrace();
				return;
			}
		}
	}
	
	/**
	 * Add the file to the network. 
	 * @param localPath Local path of where the file 
	 * @param uploadPath The upload path
	 * @param uploadName The upload name
	 * @throws RemoteException
	 * @throws IOException
	 */
	public void addFileToNetwork(String localPath, String uploadPath, 
			String uploadName) throws RemoteException, IOException{
		if (!connectToServer()) {
			throw new RemoteException("Could not connect to network. Try again later.");
		}
		
		FilePacket packet = new FilePacket(localPath);
		packet.readIn();
		
		String fileHash = FileHasher.getHashValue(localPath);
		if (fileHash == null){
			//something bad happened
			//throw something
		}
		
		File file = FileFactory.addFile(root, uploadPath, uploadName, fileHash);
		
		OutputStream out = new FileOutputStream(getFilename(fileHash));
		
		packet.writeTo(out);
		
		out.flush();
		out.close();
		
		try {
			server.addContent(this, file);
			localFiles.add(file);
		} catch (NotPrimaryServerException e) {
//			 connected to the wrong server, attempt connection to primary
			if (!connectToServer()){
				throw new RemoteException("Could not connect to network. Try again later.");
			}
			try{
				server.addContent(this, file);
				localFiles.add(file);
			} catch (NotPrimaryServerException e2) {
				//fail if it doesn't work the second time
				throw new RemoteException("Could not connect to network. Try again later.");
			}
		}
	}
	
	public void removeFileFromNetwork(File file) throws RemoteException {
		if (!connectToServer()) {
			throw new RemoteException("Could not connect to network. Try again later.");
		}
		
		try {
			server.deleteContent(file);
			localFiles.remove(file);
		} catch (NotPrimaryServerException e) {
//			 connected to the wrong server, attempt connection to primary
			if (!connectToServer()){
				throw new RemoteException("Could not connect to network. Try again later.");
			}
			try{
				server.deleteContent(file);
				localFiles.remove(file);
			} catch (NotPrimaryServerException e2) {
				//fail if it doesn't work the second time
				throw new RemoteException("Could not connect to network. Try again later.");
			}
		}
	}
	
	public void addDirectoryToNetwork(String uploadPath, String dirName) 
			throws RemoteException {
		if (!connectToServer()) {
			throw new RemoteException("Could not connect to network. Try again later.");
		}
		
		Directory dir = FileFactory.addDirectory(root, uploadPath, dirName);
		try {
			server.addDirectory(this, dir);
		} catch (NotPrimaryServerException e) {
//			 connected to the wrong server, attempt connection to primary
			if (!connectToServer()){
				throw new RemoteException("Could not connect to network. Try again later.");
			}
			try{
				server.addDirectory(this, dir);
			} catch (NotPrimaryServerException e2) {
				//fail if it doesn't work the second time
				throw new RemoteException("Could not connect to network. Try again later.");
			}
		}
	}
	
	public void removeDirectoryFromNetwork(Directory dir) throws RemoteException {
		if (!connectToServer()) {
			throw new RemoteException("Could not connect to network. Try again later.");
		}
		
		try {
			server.deleteDirectory(dir);
		} catch (NotPrimaryServerException e) {
//			 connected to the wrong server, attempt connection to primary
			if (!connectToServer()){
				throw new RemoteException("Could not connect to network. Try again later.");
			}
			try{
				server.deleteDirectory(dir);
			} catch (NotPrimaryServerException e2) {
				//fail if it doesn't work the second time
				throw new RemoteException("Could not connect to network. Try again later.");
			}
		}
	}
	
	public Directory getRoot(){
		return this.root;
	}
	
	public void setTreeModel(FileTreeModel model){
		this.treeModel = model;
	}
	
	// if a reference lease expires from server and is not renewed
	// try connecting to the other server
	public void unreferenced() {
		System.err.println("Server stopped responding ... attempting to reconnect ...");
		this.connectToServer();
	}
	
	private class LocalContentFilter implements FilenameFilter{

		java.io.File localStorage;
		
		public LocalContentFilter(java.io.File localStore){
			localStorage = localStore;
		}
		
		public boolean accept(java.io.File directory, String filename) {
			if (!localStorage.equals(directory)){
				return false;
			}
			
			//check that it's a hex value
			BigInteger bi;
			try {
				bi = new BigInteger(filename, 16);
			} catch (NumberFormatException e){
				return false;
			}
			
			if (filename.length() % 2 != 0) {
				//all our hashes have even lengths
				return false;
	        }
			
			return true;
		}
		
	}

	public void forceReconnect() throws RemoteException {
		connectToServer();
	}
}
